package com.dbflow5.processor.definition.behavior;

import com.dbflow5.data.Blob;
import com.dbflow5.processor.ProcessorManager;
import com.dbflow5.processor.definition.TypeConverterDefinition;
import com.dbflow5.processor.definition.column.ColumnAccessor;
import com.dbflow5.processor.definition.column.ColumnDefinition;
import com.dbflow5.processor.utils.ProcessorUtils;
import com.squareup.javapoet.ArrayTypeName;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;

import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;

/**
 * Description: Consolidates a column's wrapping behavior.
 */
public class ComplexColumnBehavior {

    private final TypeName columnClassName;

    /**
     * The parent column if within a [ReferenceDefinition], or the column itself if a [ColumnDefinition].
     */
    private final ColumnDefinition columnDefinition;

    /**
     * The column that it is referencing for type information if its a [ReferenceDefinition].
     * It's itself if its a [ColumnDefinition].
     */
    private final ColumnDefinition referencedColumn;

    private final boolean referencedColumnHasCustomConverter;
    public ClassName typeConverterClassName;
    public TypeMirror typeMirror;
    private final ProcessorManager manager;


    public boolean hasCustomConverter = false;
    public boolean hasTypeConverter = false;

    public ColumnAccessor wrapperAccessor = null;
    public TypeName wrapperTypeName = null;

    // Wraps for special cases such as for a Blob converter since we cannot use conventional converter
    public ColumnAccessor subWrapperAccessor = null;

    public ComplexColumnBehavior(TypeName columnClassName, ColumnDefinition columnDefinition,
                                 ColumnDefinition referencedColumn, boolean referencedColumnHasCustomConverter,
                                 ClassName typeConverterClassName, TypeMirror typeMirror, ProcessorManager manager) {
        this.columnClassName = columnClassName;
        this.columnDefinition = columnDefinition;
        this.referencedColumn = referencedColumn;
        this.referencedColumnHasCustomConverter = referencedColumnHasCustomConverter;
        this.typeConverterClassName = typeConverterClassName;
        this.typeMirror = typeMirror;
        this.manager = manager;

        handleSpecifiedTypeConverter(typeConverterClassName, typeMirror);
        evaluateIfWrappingNecessary(referencedColumn.element, manager);
    }

    private void handleSpecifiedTypeConverter(ClassName typeConverterClassName, TypeMirror typeMirror) {
        if (typeConverterClassName != null && typeMirror != null &&
            typeConverterClassName != com.dbflow5.processor.ClassNames.TYPE_CONVERTER) {
            evaluateTypeConverter(new TypeConverterDefinition(null, typeConverterClassName, typeMirror, manager, false), true);
        }
    }

    private void evaluateIfWrappingNecessary(Element element, ProcessorManager processorManager) {
        TypeName elementTypeName = referencedColumn.elementTypeName;
        if (!hasCustomConverter) {
            TypeElement typeElement = ProcessorUtils.getTypeElement(element);
            if (typeElement != null && typeElement.getKind() == ElementKind.ENUM) {
                wrapperAccessor = new ColumnAccessor.EnumColumnAccessor(elementTypeName, null);
                wrapperTypeName = ClassName.get(String.class);
            } else if (elementTypeName == ClassName.get(Blob.class)) {
                wrapperAccessor = new ColumnAccessor.BlobColumnAccessor(null);
                wrapperTypeName = ArrayTypeName.of(TypeName.BYTE);
            } else {
                if (elementTypeName instanceof ParameterizedTypeName ||
                        elementTypeName.equals(ArrayTypeName.of(TypeName.BYTE.unbox()))) {
                    // do nothing, for now.
                } else if (elementTypeName instanceof ArrayTypeName) {
                    processorManager.messager.printMessage(Diagnostic.Kind.ERROR,
                        "Columns cannot be of array type. Found "+elementTypeName);
                } else {
                    if(elementTypeName == TypeName.BOOLEAN) {
                        wrapperAccessor = new ColumnAccessor.BooleanColumnAccessor(null);
                        wrapperTypeName = TypeName.BOOLEAN;
                    }else if(elementTypeName == TypeName.CHAR) {
                        wrapperAccessor = new ColumnAccessor.CharColumnAccessor(null);
                        wrapperTypeName = TypeName.CHAR;
                    }else if(elementTypeName == TypeName.BYTE) {
                        wrapperAccessor = new ColumnAccessor.ByteColumnAccessor(null);
                        wrapperTypeName = TypeName.BYTE;
                    }else {
                        TypeConverterDefinition definition = processorManager.getTypeConverterDefinition(elementTypeName);
                        evaluateTypeConverter(definition, referencedColumnHasCustomConverter);
                    }
                }
            }
        }
    }

    private void evaluateTypeConverter(TypeConverterDefinition typeConverter, boolean isCustom) {
        // Any annotated members, otherwise we will use the scanner to find other ones
        if(typeConverter != null) {
            if (typeConverter.modelTypeName != columnClassName) {
                manager.logError("The specified custom TypeConverter's Model Value " +
                        "${typeConverter.modelTypeName} from ${typeConverter.className}" +
                        " must match the type of the column $columnClassName.");
            } else {
                hasTypeConverter = true;
                hasCustomConverter = isCustom;

                String fieldName;
                if (hasCustomConverter) {
                    fieldName = columnDefinition.entityDefinition
                            .addColumnForCustomTypeConverter(columnDefinition, typeConverter.className);
                } else {
                    fieldName = columnDefinition.entityDefinition
                            .addColumnForTypeConverter(columnDefinition, typeConverter.className);
                }

                wrapperAccessor = new ColumnAccessor.TypeConverterScopeColumnAccessor(fieldName, null);
                wrapperTypeName = typeConverter.dbTypeName;

                // special case of blob
                if (wrapperTypeName == ClassName.get(Blob.class)) {
                    subWrapperAccessor = new ColumnAccessor.BlobColumnAccessor(null);
                }
            }
        }
    }
}